
/*
 * sndpipe: A UNIX pipes-based audio stream processor toolkit.
 * Copyright (C) 2011  Bradley Worley
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License
 * as published by the Free Software Foundation; either version 2
 * of the License, or (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 * 
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to:
 *
 *   Free Software Foundation, Inc.
 *   51 Franklin Street, Fifth Floor
 *   Boston, MA  02110-1301, USA.
 */

/* include the sndpipe header. */
#include "sndpipe.h"

/* define the default hardware capture/output buffer size. */
#define HW_BUF_SIZE  256

/* define the hardware audio format for parsing alsa formats. */
struct hw_format {
  /* declare the format string and integer code. */
  const char *str;
  int fmtid;
};

/* declare the array of accepted hardware audio formats. */
static struct hw_format formats[] = {
  { "S8", SND_PCM_FORMAT_S8 },
  { "U8", SND_PCM_FORMAT_U8 },
  { "S16_LE", SND_PCM_FORMAT_S16_LE },
  { "S16_BE", SND_PCM_FORMAT_S16_BE },
  { "U16_LE", SND_PCM_FORMAT_U16_LE },
  { "U16_BE", SND_PCM_FORMAT_U16_BE },
  { "S24_LE", SND_PCM_FORMAT_S24_LE },
  { "S24_BE", SND_PCM_FORMAT_S24_BE },
  { "U24_LE", SND_PCM_FORMAT_U24_LE },
  { "U24_BE", SND_PCM_FORMAT_U24_BE },
  { "S32_LE", SND_PCM_FORMAT_S32_LE },
  { "S32_BE", SND_PCM_FORMAT_S32_BE },
  { "U32_LE", SND_PCM_FORMAT_U32_LE },
  { "U32_BE", SND_PCM_FORMAT_U32_BE },
  { SP_UNKNOWN, SP_UNKNOWN }
};

struct hardware *hw_open_input (unsigned int rate) {
  /* declare required variables. */
  struct hardware *hw;
  unsigned int hw_rate;
  int fmt;
  char *dev;

  /* allocate memory for the hardware structure. */
  hw = (struct hardware *) malloc (sizeof (struct hardware));

  /* ensure the hardware structure memory was allocated. */
  if (!hw) {
    /* print a failure message and return a null result. */
    warnf ("unable to allocate input device memory");
    return NULL;
  }

  /* read the hardware device name from the arguments array. */
  dev = opts_get_string (OPTS_S_DEVICE);

  /* ensure we were passed a device name. */
  if (!dev) {
    /* print a failure message and return a null result. */
    warnf ("unable to retrieve input hardware device name");
    return NULL;
  }

  /* attempt to open the pcm capture device. */
  if (snd_pcm_open (&hw->dh, dev, SND_PCM_STREAM_CAPTURE, 0) < 0) {
    /* print a failure message and return a null result. */
    warnf ("unable to open input hardware");
    return NULL;
  }

  /* attempt to allocate memory for the hardware parameters. */
  if (snd_pcm_hw_params_malloc (&hw->parms) < 0) {
    /* print a failure message and return a null result. */
    warnf ("unable to allocate input hardware parameters");
    return NULL;
  }

  /* attempt to set the default parameter set. */
  if (snd_pcm_hw_params_any (hw->dh, hw->parms) < 0) {
    /* print a failure message and return a null result. */
    warnf ("unable to determine default input hardware parameters");
    return NULL;
  }

  /* attempt to set interleaved access to the device. */
  if (snd_pcm_hw_params_set_access (hw->dh, hw->parms,
        SND_PCM_ACCESS_RW_INTERLEAVED) < 0) {
    /* print a failure message and return a null result. */
    warnf ("unable to set interleaved input device access");
    return NULL;
  }

  /* parse the hardware format string. */
  fmt = hw_get_format ();

  /* ensure we successfully parsed the format string. */
  if (fmt == SND_PCM_FORMAT_UNKNOWN) {
    /* print a failure message and return a null result. */
    warnf ("unable to determine hardware input format");
    return NULL;
  }

  /* attempt to set the hardware device sample format. */
  if (snd_pcm_hw_params_set_format (hw->dh, hw->parms, fmt) < 0) {
    /* print a failure message and return a null result. */
    warnf ("unable to set input hardware format");
    return NULL;
  }

  /* set the initial guess of the sample rate. */
  hw_rate = rate;

  /* attempt to set the hardware device sample rate. */
  if (snd_pcm_hw_params_set_rate_near (hw->dh, hw->parms, &hw_rate, 0) < 0 ||
      hw_rate != rate) {
    /* print a failure message and return a null result. */
    warnf ("unable to set requested hardware sample rate");
    return NULL;
  }

  /* read the number of channels from the arguments array. */
  hw->channels = opts_get_int (OPTS_S_CHANNELS);

  /* ensure the number of channels is at least one. */
  if (!hw->channels)
    hw->channels = 1;

  /* attempt to set the number of channels on the device. */
  if (snd_pcm_hw_params_set_channels (hw->dh, hw->parms, hw->channels) < 0) {
    /* print a failure message and return a null result. */
    warnf ("unable to set input hardware channel count");
    return NULL;
  }

  /* attempt to set the final set of hardware parameters. */
  if (snd_pcm_hw_params (hw->dh, hw->parms) < 0) {
    /* print a failure message and return a null result. */
    warnf ("unable to set new input hardware parameters");
    return NULL;
  }

  /* free the hardware parameters. */
  snd_pcm_hw_params_free (hw->parms);

  /* attempt to prepare the device for input. */
  if (snd_pcm_prepare (hw->dh) < 0) {
    /* print a failure message and return a null result. */
    warnf ("unable to prepare input hardware for reading");
    return NULL;
  }

  /* return the newly allocated and initialized hardware input device. */
  return hw;
}

struct hardware *hw_open_output (void) {
  /* FIXME: implement hw_open_output() */
  return NULL;
}

/* hw_close: closes an input or output hardware structure.
 * @hw: the hardware structure to close and free.
 *
 * this function closes a hardware structure allocated with hw_open_output()
 * or hw_open_input() and frees its contained memory.
 */
int hw_close (struct hardware *hw) {
  /* ensure we have a structure. */
  if (!hw || !hw->dh) {
    /* print a warning message and return a failure code. */
    warnf ("null hardware structure passed to hw_close");
    return SP_FAIL;
  }

  /* stop the device, if possible. */
  if (snd_pcm_drop (hw->dh) < 0) {
    /* print a warning message and return a failure code. */
    warnf ("unable to stop closing hardware device");
    return SP_FAIL;
  }

  /* close the device. */
  if (snd_pcm_close (hw->dh) < 0) {
    /* print a warning message and return a failure code. */
    warnf ("unable to close hardware device");
    return SP_FAIL;
  }

  /* return a success code. */
  return SP_OK;
}

int hw_write (struct hardware *hw, double **buf, unsigned long frames) {
  /* FIXME: implement hw_write() */
  return SP_FAIL;
}

/* hw_read: reads data into a buffer from a hardware capture device.
 * @hw: the input hardware struct to read from.
 * @buf: a pointer to the buffer to write data into while reading.
 * @frames: the number of frames of data in the buffer.
 *
 * this function reads data in interleaved form into a buffer from an input
 * hardware struct's capture device, where the final data is stored in non-
 * interleaved form. an SP_OK status is returned so long as the number of
 * frames read equals the number of frames specified by the calling function.
 */
int hw_read (struct hardware *hw, double ***buf, unsigned long frames) {
  /* declare required variables. */
  unsigned long i, c, count;

  /* ensure proper starting information. */
  if (!hw || !hw->channels || !(*buf) || !frames) {
    /* print a failure message and exit the application. */
    failf ("insufficient information passed to hw_read");
  }

  /* check if we need to allocate our read buffer. */
  if (!hw->buf) {
    /* set the buffer size based on the frame and channel counts. */
    hw->buf_size = frames * hw->channels;

    /* allocate new memory for the buffer. */
    hw->buf = (int *) malloc (sizeof (int) * hw->buf_size);

    /* ensure the buffer memory was allocated. */
    if (!hw->buf) {
      /* print a failure message and exit the application. */
      failf ("unable to allocate read buffer memory");
    }
  }

  /* read in the interleaved data from the capture device. */
  while ((count = snd_pcm_readi (hw->dh, hw->buf, hw->buf_size)) < 0) {
    /* print a warning message and re-prepare the buffer. */
    warnf ("hardware buffer over-run");
    snd_pcm_prepare (hw->dh);
  }

  /* loop through the input channels. */
  for (c = 0; c < hw->channels; c++) {
    /* and through the frames of each input channel. */
    for (i = 0; i < frames; i++) {
      /* copy the interleaved data out of the hardware buffer. */
      (*buf)[c][i] = (double) hw->buf[c + i * hw->channels] / INT_MAX;
    }
  }

  /* return a status code based on the number of frames read. */
  return (count == hw->buf_size ? SP_OK : SP_FAIL);
}

/* hw_buf_alloc: allocate memory for a buffer based on hardware.
 * @hw: the hardware structure to base the buffer on.
 * @frames: a pointer to the frame count of the buffer.
 *
 * this function allocates a multichannel buffer based on a hardware
 * structure. the frames variable is a pointer to the number of frames
 * desired in each channel. if *frames is zero, a default value is used.
 */
double **hw_buf_alloc (struct hardware *hw, unsigned long *frames) {
  /* declare required variables. */
  unsigned long c;
  double **buf;

  /* set *frames to a default value if its set to zero. */
  if (!(*frames))
    *frames = HW_BUF_SIZE;

  /* ensure that we have enough hardware information. */
  if (!hw || !hw->channels) {
    /* print a failure message and exit the application. */
    failf ("incomplete hardware information passed to hw_buf_alloc");
  }

  /* allocate memory for the hardware buffer channels. */
  buf = (double **) malloc (sizeof (double *) * hw->channels);

  /* ensure we allocated memory for the hardware buffer channels. */
  if (!buf) {
    /* print a failure message and exit the application. */
    failf ("unable to allocate buffer channel array memory");
  }

  /* loop through the buffer channels. */
  for (c = 0; c < hw->channels; c++) {
    /* allocate memory for the channel frames. */
    buf[c] = (double *) malloc (sizeof (double) * (*frames));

    /* ensure we allocated memory for the frames. */
    if (!buf[c]) {
      /* print a failure message and exit the application. */
      failf ("unable to allocate memory for buffer frames");
    }
  }

  /* return the newly allocated memory. */
  return buf;
}

/* hw_buf_free: frees and allocated hardware-associated buffer.
 * @hw: the hardware struct to use information from.
 * @frames: the number of frames in the buffer.
 * @buf: the hardware buffer to free.
 *
 * this function frees buffers that have been allocated by the corresponding
 * hw_buf_alloc() function. the arguments and usage are similar to that
 * function.
 */
void hw_buf_free (struct hardware *hw, unsigned long frames, double **buf) {
  /* declare required variables. */
  unsigned long c;

  /* ensure that we have enough hardware information. */
  if (!hw || !hw->channels || !buf || !frames) {
    /* print a failure message and exit the application. */
    failf ("incomplete hardware information passed to hw_buf_free");
  }

  /* loop through the buffer channels, freeing the frames. */
  for (c = 0; c < hw->channels; c++)
    free (buf[c]);

  /* free the buffer channels. */
  free (buf);
}

/* hw_get_format: parses a string for hardware audio format information.
 *
 * this function pulls its information from the format argument in the
 * options parsing array. values are compared to accepted values and, if
 * a match is found, the corresponding alsa hardware format integer is
 * returned. otherwise, a -1 value is returned.
 */
int hw_get_format (void) {
  /* declare required variables. */
  char *fmt;
  int i;
  
  /* get the format string from the parsed command-line arguments. */
  fmt = opts_get_string (OPTS_S_FORMAT);
  
  /* ensure the format string is non-null. */
  if (!fmt)
    return SND_PCM_FORMAT_UNKNOWN;

  /* loop through the array of known format strings. */
  for (i = 0; formats[i].str; i++) {
    /* see if the currently indexed known format string is a match. */
    if (strcmp (fmt, formats[i].str) == 0)
      return formats[i].fmtid;
  }
  
  /* return unknown. we failed to find a match. */
  return SND_PCM_FORMAT_UNKNOWN;
}

